package com.nc.mem.serial.graph;

import gnu.trove.map.custom_hash.TObjectIntCustomHashMap;
import gnu.trove.map.hash.TIntObjectHashMap;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.text.MessageFormat;

public final class Context implements Cloneable {

	private static final ThreadLocal<Context> CONTEXTS = new ThreadLocal<Context>() {

		@Override
		protected final Context initialValue() {

			return new Context();
		}
	};

	public static final Context current() {
		return CONTEXTS.get();
	}

	private final TObjectIntCustomHashMap<Object> objToId;
	private final TIntObjectHashMap<Object> idToObj;
	int seq;
	int depth;

	public Context() {
		objToId = new TObjectIntCustomHashMap<Object>(Representation.IDENTITY_HASHING, 4, .5f, -1);
		idToObj = new TIntObjectHashMap<Object>(4, .5f, -1);
	}

	private Context(final TIntObjectHashMap<Object> idToObj) {
		this.objToId = new TObjectIntCustomHashMap<Object>(null, 0);
		this.idToObj = clone(idToObj);
	}

	public void clear() {
		depth--;
		if (depth == 0) {
			objToId.clear();
			idToObj.clear();
			seq = 0;
		}
	}

	@Override
	public Context clone() {
		final Context c = new Context(idToObj);

		return c;
	}

	private TIntObjectHashMap<Object> clone(final TIntObjectHashMap<Object> map) {
		final TIntObjectHashMap<Object> ret = new TIntObjectHashMap<Object>(map.capacity(), .5f, -1);

		ret.putAll(map);

		return ret;
	}

	public void dumpMapSizes(final PrintStream pw) {

		pw.println("IdToObj: " + idToObj.size());
		pw.println("ObjToId: " + objToId.size());
	}

	public void dumpMapSizes(final PrintWriter pw) {

		pw.println("IdToObj: " + idToObj.size());
		pw.println("ObjToId: " + objToId.size());
	}

	public Object fromId(final int id) {
		return (id & 1) != 0 ? null : idToObj.get(id & ~(1 << 1));
	}

	public int fromObject(final Object o) {
		return o == null ? 1 : objToId.get(o);
	}

	public void registerId(final Object obj, final int id) {
		if ((id & 1) == 0) {
			final int s = id & ~(1 << 1);
			idToObj.put(s, obj);
		}
	}

	/**
	 * <ul>
	 * <li>The first lower order bit is used to tag a null value. (Only used for collections)</li>
	 * <li>The second lower order bit is used to tag if the object is a pointer, that is, it has
	 * already been previously serialized.</li>
	 * <li>The remaining 30 bits are used for the actual ids.</li>
	 * <ul>
	 * The first time an object is seriliazed, it won't be tagged, therefore upon deserialization
	 * we'll know we must deserialize it further or get it's value from the map. <br>
	 * The choice of lower order bits for tagging is purelly for performance, since ints will end up
	 * packed as bytes if they are composed of lower order bits only.
	 */
	public int registerObject(final Object obj) {
		if (obj != null) {
			int s = objToId.get(obj);
			if (s == -1) {
				s = ++seq << 2 & ~(1 << 1);
				objToId.put(obj, s);
			} else {
				s |= 1 << 1;
			}
			return s;
		}
		return 1;
	}

	public Context start() {
		depth++;
		return this;
	}

	@Override
	public String toString() {
		return MessageFormat.format("Context [objectToId={0}, idToObject={1}, seq={2}]", objToId, idToObj, seq);
	}

}